"use strict"
import {
	produce,
	applyPatches,
	immerable,
	produceWithPatches,
	enableMapSet,
	enablePatches,
	setAutoFreeze
} from "../src/immer"

enableMapSet()
enablePatches()

describe("readme example", () => {
	it("works", () => {
		const baseState = [
			{
				todo: "Learn typescript",
				done: true
			},
			{
				todo: "Try immer",
				done: false
			}
		]

		const nextState = produce(baseState, draft => {
			draft.push({todo: "Tweet about it"})
			draft[1].done = true
		})

		// the new item is only added to the next state,
		// base state is unmodified
		expect(baseState.length).toBe(2)
		expect(nextState.length).toBe(3)

		// same for the changed 'done' prop
		expect(baseState[1].done).toBe(false)
		expect(nextState[1].done).toBe(true)

		// unchanged data is structurally shared
		expect(nextState[0]).toBe(baseState[0])
		// changed data not (dûh)
		expect(nextState[1]).not.toBe(baseState[1])
	})

	it("patches", () => {
		let state = {
			name: "Micheal",
			age: 32
		}

		// Let's assume the user is in a wizard, and we don't know whether
		// his changes should be updated
		let fork = state
		// all the changes the user made in the wizard
		let changes = []
		// all the inverse patches
		let inverseChanges = []

		fork = produce(
			fork,
			draft => {
				draft.age = 33
			},
			// The third argument to produce is a callback to which the patches will be fed
			(patches, inversePatches) => {
				changes.push(...patches)
				inverseChanges.push(...inversePatches)
			}
		)

		// In the mean time, our original state is updated as well, as changes come in from the server
		state = produce(state, draft => {
			draft.name = "Michel"
		})

		// When the wizard finishes (successfully) we can replay the changes made in the fork onto the *new* state!
		state = applyPatches(state, changes)

		// state now contains the changes from both code paths!
		expect(state).toEqual({
			name: "Michel",
			age: 33
		})

		// Even after finishing the wizard, the user might change his mind...
		state = applyPatches(state, inverseChanges)
		expect(state).toEqual({
			name: "Michel",
			age: 32
		})
	})

	it("can update set", () => {
		const state = {
			title: "hello",
			tokenSet: new Set()
		}

		const nextState = produce(state, draft => {
			draft.title = draft.title.toUpperCase()
			draft.tokenSet.add("c1342")
		})

		expect(state).toEqual({title: "hello", tokenSet: new Set()})
		expect(nextState).toEqual({
			title: "HELLO",
			tokenSet: new Set(["c1342"])
		})
	})

	it("can deep update map", () => {
		const state = {
			users: new Map([["michel", {name: "miche", age: 27}]])
		}

		const nextState = produce(state, draft => {
			draft.users.get("michel").name = "michel"
		})

		expect(state).toEqual({
			users: new Map([["michel", {name: "miche", age: 27}]])
		})
		expect(nextState).toEqual({
			users: new Map([["michel", {name: "michel", age: 27}]])
		})
	})

	it("supports immerable", () => {
		class Clock {
			constructor(hours = 0, minutes = 0) {
				this.hours = hours
				this.minutes = minutes
			}

			increment(hours, minutes = 0) {
				return produce(this, d => {
					d.hours += hours
					d.minutes += minutes
				})
			}

			toString() {
				return `${("" + this.hours).padStart(2, 0)}:${(
					"" + this.minutes
				).padStart(2, 0)}`
			}
		}
		Clock[immerable] = true

		const midnight = new Clock()
		const lunch = midnight.increment(12, 30)

		expect(midnight).not.toBe(lunch)
		expect(lunch).toBeInstanceOf(Clock)
		expect(midnight.toString()).toBe("00:00")
		expect(lunch.toString()).toBe("12:30")

		const diner = lunch.increment(6)

		expect(diner).not.toBe(lunch)
		expect(lunch).toBeInstanceOf(Clock)
		expect(diner.toString()).toBe("18:30")
	})

	test("produceWithPatches", () => {
		const result = produceWithPatches(
			{
				age: 33
			},
			draft => {
				draft.age++
			}
		)
		expect(result).toEqual([
			{
				age: 34
			},
			[
				{
					op: "replace",
					path: ["age"],
					value: 34
				}
			],
			[
				{
					op: "replace",
					path: ["age"],
					value: 33
				}
			]
		])
	})
})

test("Producers can update Maps", () => {
	setAutoFreeze(true)
	const usersById_v1 = new Map()

	const usersById_v2 = produce(usersById_v1, draft => {
		// Modifying a map results in a new map
		draft.set("michel", {name: "Michel Weststrate", country: "NL"})
	})

	const usersById_v3 = produce(usersById_v2, draft => {
		// Making a change deep inside a map, results in a new map as well!
		draft.get("michel").country = "UK"
		debugger
	})

	// We got a new map each time!
	expect(usersById_v2).not.toBe(usersById_v1)
	expect(usersById_v3).not.toBe(usersById_v2)
	// With different content obviously
	expect(usersById_v1).toMatchInlineSnapshot(`Map {}`)
	expect(usersById_v2).toMatchInlineSnapshot(`
		Map {
		  "michel" => {
		    "country": "NL",
		    "name": "Michel Weststrate",
		  },
		}
	`)
	expect(usersById_v3).toMatchInlineSnapshot(`
		Map {
		  "michel" => {
		    "country": "UK",
		    "name": "Michel Weststrate",
		  },
		}
	`)
	// The old one was never modified
	expect(usersById_v1.size).toBe(0)
	// And trying to change a Map outside a producers is going to: NO!
	expect(() => usersById_v3.clear()).toThrowErrorMatchingSnapshot()
})

test("clock class", () => {
	class Clock {
		[immerable] = true

		constructor(hour, minute) {
			this.hour = hour
			this.minute = minute
		}

		get time() {
			return `${this.hour}:${this.minute}`
		}

		tick() {
			return produce(this, draft => {
				draft.minute++
			})
		}
	}

	const clock1 = new Clock(12, 10)
	const clock2 = clock1.tick()
	expect(clock1.time).toEqual("12:10") // 12:10
	expect(clock2.time).toEqual("12:11") // 12:11
	expect(clock2).toBeInstanceOf(Clock)
})
